使用者が手数料を支払わなくても使える Ethereum ベースのトークン
Summary
使用者が手数料(ガス)を支払わなくても使える Ethereum ベースの ERC-20 標準互換のトークンを考えたよ
遅くなったけど夏休みの自由研究をまとめた
使用者(== トークン所有者)が手数料を支払うのではなく、第三者(e.g. トークン受取側)が手数料を支払うことができる
トークンのスマートコントラクトとサービスのプロトタイプを実装してみた
背景・課題
ERC-20 トークンのユースケース: 「地域通貨」や「特定の店舗・サービスで使用可能なトークン」を発行できる
だけど、現状ではほとんどの使用者が Ether を所有していない!
つまり、ERC-20 トークンを他のアカウントに送金するためのトランザクション手数料(ガス)を支払うことができない!
この課題を解決するためには次のような方法が考えられるけど、それぞれ問題がありそう。
トークンの配布時に一緒に Ether を配る
=> ソリューションとしてイケてないと思うけど、Ether を普及させるという意味では一定の価値はあるかも?
サービス運営者が秘密鍵・トランザクションを管理
=> GOX リスクと Ethereum ブロックチェーンを使用する意味がかなり小さくなってしまいそう。
そもそも、上記のユースケースについて Ethereum(ブロックチェーン)を使用すべきか問題については、また別の議題
新たな課題解決方法「代理トランザクション」
使用者(== トークン所有者)が「所持しているトークンを誰にどのくらい送るか」という情報に対して署名のみを行う
第三者が、トークン所有者の署名を使用して、Ethereum トランザクションを作成・実行
トランザクション手数料(ガス)を支払うのは第三者
第三者が手数料を支払うインセンティブ
トークンを受け取るとき(第三者 == トークンの受取り手)
トークンをユーザに使用してもらいたい事業者
今回作成した代理トランザクションを実現するための具体的な流れと仕組み
トークン所有者: トークンの送金情報(独自フォーマットのトランザクションデータ)に署名
トークントランザクションデータ
nonce : transfer を実行した回数
value : 送金トークンの量
to : トークンの送付先
tokenAddress : トークンのコントラクトアドレス
トークン所有者: トークントランザクションデータとその署名を、トランザクションを代理で実行してくれる第三者に渡す
ここでは、トランザクションを代理してくれる第三者を「トランザクション代理人」とします。
トランザクション代理人: トークントランザクションデータと署名を引数にトークンのスマートコントラクトの関数を実行
通常の ERC-20 トークンに、delegatedTransfer() という関数を追加
トークン所有者が署名したトークントランザクションデータと署名を引数にとる
ecrecover で署名検証をして、トークントランザクションデータと署名が正しければトークンの送金を実行
さらに、トークンのスマートコントラクトに、リプレイアタックを防ぐための nonce を追加
nonce はトークンが 送金されるたびにインクリメントされる
トークンのトランザクションデータとその署名を得るコード(一部)
code:CreateAndSignTokenTx.js
// web3を初期化
const web3 = new Web3(WEB3_PROVIDER)
// トークンのスマートコントラクトを初期化
const token = new web3.eth.Contract(TOKEN_ABI, TOKEN_ADDRESS)
// トークンを送信するためのトークントランザクションデータを作成
function createTokenTx(nonce, value, to, tokenAddress) {
const hexNonce = Web3.utils.padLeft(Web3.utils.numberToHex(nonce), 64)
const hexValue = Web3.utils.padLeft(Web3.utils.numberToHex(value), 64)
}
// ハッシュに署名をして独自フォーマットに整形します。
function signTokenTx(hash, privateKey) {
const sig = EthUtil.ecsign(hash, privateKey)
const r = EthUtil.bufferToHex(sig.r)
const s = EthUtil.bufferToHex(sig.s)
const v = Web3.utils.numberToHex(sig.v)
}
async function main() {
// トークンの nonce を取得
let tokenNonce = await token.methods.nonceOf(ADDRESS).call()
// トークントランザクションデータの作成
const tokenTx = createTokenTx(tokenNonce, VALUE, TO, TOKEN_ADDRESS)
console.log('tokenTx', tokenTx)
// トークントランザクションの Keccak256 ハッシュを取得
const hash = EthUtil.sha3(tokenTx)
// 署名を取得
const tokenSig = signTokenTx(hash, EthUtil.toBuffer(PRIVATE_KEY_STRING))
console.log('tokenSig', tokenSig)
}
main()
トークンのスマートコントラクトの実装(一部)
code:TokenContract.sol
function delegatedTransfer(bytes delegatedTx, bytes sig)
public
returns (bool)
{
// 署名 sig を分割して、r,s,v を取り出す
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(sig, 32))
s := mload(add(sig, 64))
v := byte(0, mload(add(sig, 96)))
}
// ecrecover を使用して、署名者のアドレスを得る
bytes32 txHash = keccak256(bytes(delegatedTx));
address signer = ecrecover(txHash, v, r, s);
require(signer != address(0));
// 独自フォーマットのトークントランザクションをデコード
uint256 nonce;
uint256 value;
address to;
address tokenAddress;
assembly {
nonce := mload(add(delegatedTx, 32))
value := mload(add(delegatedTx, 64))
to := mload(add(delegatedTx, 84))
tokenAddress := mload(add(delegatedTx, 116))
}
// トークンのアドレスとコントラクトのアドレスが一致しているかどうかをチェック
require(tokenAddress == address(this));
// トークン送金先が 0x0 でないことをチェック
require(to != address(0));
// nonce をチェック
require(nonce == _noncessigner); // 送金トークン数が残高以下であることをチェック
require(value <= _balancessigner); // nonce インクリメントのオーバーフロー対策
// nonce をインクリメント
// 送金元の残高から送金トークン数を差し引く
// require(value <= _balancessigner); により、 // value が _balancessigner 以下であることが保証済 // 送金先の残高と送金トークン数の合計値が、現在の残高以上であることをチェック
// オーバーフロー対策
require(_balancesto + value >= _balancesto); // 送金先の残高に送金トークン数を加える
_balancesto = _balancesto + value; // イベントを発火
emit Transfer(signer, to, value);
return true;
}
代理でトランザクションを実行するコード(一部)
code:ExecDelegatedTransfer.js
// web3を初期化
const web3 = new Web3(WEB3_PROVIDER)
// トークンのスマートコントラクトを初期化
const token = new web3.eth.Contract(TOKEN_ABI, TOKEN_ADDRESS)
async function main() {
// 秘密鍵からウォレットを生成
web3.eth.accounts.wallet.add(PRIVATE_KEY_STRING);
// delegatedTransfer メソッドを実行
let options = {
from: ADDRESS,
gasPrice: 40000000000,
gas: 100000
}
let result = await token.methods.delegatedTransfer(TOKEN_TX, TOKEN_SIG).send(options)
console.log(result)
}
main()
実際に実行したトランザクション
手軽に代理トランザクションを遊べるプロトタイプ(Ropsten)
Google アカウントでログインすると自動的に wallet が作成されます。
他のひとにトークンの Transaction を Delegate(委譲)することができます。
1度に委譲できる Transaction は 1 つのみです。
Delegate した Transaction は "Sent Delegated Transfer" に表示されます。
Delegate された Transaction は "Received Delegated Transfers" に表示されます。
Delegate された Transaction を accept すると、代理で Transaction を実行できます。
代理で Transaction を実行するためには Ether が必要です。
代理トランザクションが可能なトークン D20 Token は次の URL で入手可能です(要MetaMask)
さいごに
メモ
トークン所有者が代理人を指定できる仕組みにしても面白いかも
トークントランザクションに代理人のアドレスを追加して署名するようにすればできる